package com.luo.d3s.ext.aop.log.aspect;

import com.luo.d3s.core.application.service.ApplicationService;
import com.luo.d3s.core.application.service.CommandService;
import com.luo.d3s.core.application.service.QueryService;
import com.luo.d3s.core.domain.event.DomainEventHandler;
import com.luo.d3s.core.util.JsonUtils;
import com.luo.d3s.ext.aop.config.D3sAopProps;
import com.luo.d3s.ext.aop.config.ObjectToStringType;
import com.luo.d3s.ext.aop.log.anno.D3sLog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.javatuples.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.logging.LogLevel;

import java.lang.reflect.Method;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;

/**
 * 日志切面基础类
 *
 * @author luohq
 * @date 2023-01-31 14:48
 */
public class D3sBaseLogAspect {

    /**
     * 打印连接点对应的方法日志
     *
     * @param point    连接点
     * @param logProps 日志配置属性
     * @return 连接点执行结果
     * @throws Throwable
     */
    protected Object proceedWithLog(ProceedingJoinPoint point, D3sAopProps.LogProps logProps) throws Throwable {
        //解析当前连接点信息
        CodeSignature codeSignature = (CodeSignature) point.getSignature();
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();
        Class<?> methodClass = method.getDeclaringClass();
        Logger logger = LoggerFactory.getLogger(methodClass);

        //获取当前方法日志打印配置（优先获取方法上注解 -> 然后获取类上注解）
        D3sLog d3sLogAnno = Optional.ofNullable(method.getAnnotation(D3sLog.class))
                .orElse((D3sLog) methodClass.getAnnotation(D3sLog.class));
        //若方法、类上不存在注解 且 全局日志配置不打印，则直接执行目标方法、忽略日志打印
        if (Objects.isNull(d3sLogAnno) && !matchPrintLog(methodClass, logProps)) {
            return point.proceed();
        }
        //若方法、类上注解设置了enable为false，则直接执行目标方法、忽略日志打印
        if (!Objects.isNull(d3sLogAnno) && !d3sLogAnno.enabled()) {
            return point.proceed();
        }

        //解析日志打印配置
        LogLevel logLevel = Optional.ofNullable(d3sLogAnno).map(D3sLog::logLevel).orElse(logProps.getLogLevel());
        boolean showParams = Optional.ofNullable(d3sLogAnno).map(D3sLog::showParams).orElse(logProps.getShowParams());
        boolean showResult = Optional.ofNullable(d3sLogAnno).map(D3sLog::showResult).orElse(logProps.getShowResult());
        ObjectToStringType objectToStringType = Optional.ofNullable(d3sLogAnno).map(D3sLog::objectToStringType).orElse(logProps.getObjectToStringType());
        boolean showExecutionTime = Optional.ofNullable(d3sLogAnno).map(D3sLog::showExecutionTime).orElse(logProps.getShowExecutionTime());
        ChronoUnit executionTimeUnit = Optional.ofNullable(d3sLogAnno).map(D3sLog::executionTimeUnit).orElse(logProps.getExecutionTimeUnit());

        /** 打印方法参数（入口） */
        String methodName = convertMethodName(method, methodClass, logProps);
        Object[] methodParams = point.getArgs();
        String[] methodParamNames = codeSignature.getParameterNames();
        log(logger, logLevel, entry(methodName, showParams, methodParamNames, methodParams, objectToStringType));

        /** 执行方法、统计执行时间 */
        Instant start = Instant.now();
        Object response = point.proceed();
        Instant end = Instant.now();
        String duration = String.format("%s %s", executionTimeUnit.between(start, end), executionTimeUnit.name().toLowerCase());

        /** 打印方法结果（出口） */
        log(logger, logLevel, exit(methodName, duration, response, showResult, showExecutionTime, objectToStringType));
        return response;
    }

    /**
     * 提取方法名称
     *
     * @param method      方法
     * @param methodClass 方法对应的类
     * @param logProps    日志配置属性
     * @return 待打印的方法名称
     */
    protected String convertMethodName(Method method, Class<?> methodClass, D3sAopProps.LogProps logProps) {
        if (!logProps.getShowClassName()) {
            return method.getName();
        }
        return new StringJoiner(".")
                .add(methodClass.getSimpleName())
                .add(method.getName())
                .toString();
    }

    /**
     * 匹配类是否需要打印日志
     *
     * @param methodClass 连接点方法对应的类
     * @param logProps    日志配置属性
     * @return 否需要打印日志
     */
    protected Boolean matchPrintLog(Class<?> methodClass, D3sAopProps.LogProps logProps) {
        List<Pair<Boolean, Class<?>>> matchList = Arrays.asList(
                Pair.with(logProps.getEnableCommandServiceLog(), CommandService.class),
                Pair.with(logProps.getEnableQueryServiceLog(), QueryService.class),
                Pair.with(logProps.getEnableApplicationServiceLog(), ApplicationService.class),
                Pair.with(logProps.getEnableDomainEventHandlerLog(), DomainEventHandler.class)
        );
        return matchList.stream()
                .anyMatch(matchPair -> matchPair.getValue0() && matchPair.getValue1().isAssignableFrom(methodClass));
    }

    /**
     * 拼接方法入口日志
     *
     * @param methodName         执行方法名
     * @param showParams         是否打印参数
     * @param paramNames         参数名称列表
     * @param params             参数列表
     * @param objectToStringType 对象转字符串类型
     * @return 方法入口日志
     */
    private String entry(String methodName, boolean showParams, String[] paramNames, Object[] params, ObjectToStringType objectToStringType) {
        StringJoiner message = new StringJoiner(" ")
                .add(methodName).add("Started");
        if (showParams && Objects.nonNull(paramNames) && Objects.nonNull(params) && paramNames.length == params.length) {
            Map<String, Object> values = new HashMap<>(paramNames.length);
            for (int i = 0; i < paramNames.length; i++) {
                values.put(paramNames[i], params[i]);
            }
            message.add("with params:")
                    .add(convertObjectToString(values, objectToStringType));
        }
        return message.toString();
    }

    /**
     * 拼接方法出口日志
     *
     * @param methodName         执行方法名
     * @param duration           方法执行时长
     * @param result             方法执行返回结果
     * @param showResult         是否打印返回结果
     * @param showExecutionTime  是否打印执行时长
     * @param objectToStringType 对象转字符串类型
     * @return 方法出口日志
     */
    private String exit(String methodName, String duration, Object result, boolean showResult, boolean showExecutionTime, ObjectToStringType objectToStringType) {
        StringJoiner message = new StringJoiner(" ")
                .add(methodName).add("Finished");
        if (showExecutionTime) {
            message.add("in").add(duration);
        }
        if (showResult) {
            message.add("with result:").add(convertObjectToString(result, objectToStringType));
        }
        return message.toString();
    }

    /**
     * 转换对应为字符串
     *
     * @param obj                待转换对象
     * @param objectToStringType 转换类型
     * @return 对象的字符串表示
     */
    private String convertObjectToString(Object obj, ObjectToStringType objectToStringType) {
        if (ObjectToStringType.JSON.equals(objectToStringType)) {
            return JsonUtils.toJson(obj);
        } else {
            return String.valueOf(obj);
        }
    }

    /**
     * 根据logLevel打印日志
     *
     * @param logger  日志对象
     * @param level   日志级别
     * @param message 日志内容
     */
    private void log(Logger logger, LogLevel level, String message) {
        switch (level) {
            case DEBUG:
                logger.debug(message);
                break;
            case TRACE:
                logger.trace(message);
                break;
            case WARN:
                logger.warn(message);
                break;
            case ERROR:
            case FATAL:
                logger.error(message);
                break;
            default:
                logger.info(message);
                break;
        }
    }
}
